前言
距离上一篇关于刘海投影的文章也过了一年有余https://zhuanlan.zhihu.com/p/232450616,笔者也在这期间积累了一些工作经验,于是在实习结束后的某一天灵感迸现,发现刘海投影应有更加简单且高效的做法。
效果实现原理
以模板测试为核心,原理变得更为简单了:
在绘制面部时写入特定的模板值X,然后在不透明物体绘制完之后再绘制一次头发,此时根据屏幕空间的光照方向对它的裁剪空间坐标进行偏移,并只在模板值X时通过模板测试。
不熟悉模板测试的读者可以参考以下文章:
https://zhuanlan.zhihu.com/p/28506264
本文目录为:
- 使用Render Feature额外绘制头发
- 改良-以性能换效果
- 结语
注
笔者所用Unity版本为2019.4.6f1,URP 7.3.1
笔者经验甚少,才浅学疏,难以避免文中出现错误,还请大家不吝斧正,只求轻喷。
使用Render Feature额外绘制头发
我们要先在画脸的时候写入Stencil Buffer,因此角色Shader中需添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| Properties { [Header(Stencil)] _StencilRef ("_StencilRef", Range(0, 255)) = 0 [Enum(UnityEngine.Rendering.CompareFunction)]_StencilComp ("_StencilComp", Float) = 0 }
Pass { Name "BaseCel" Tags { "LightMode" = "UniversalForward" }
Stencil { Ref [_StencilRef] Comp [_StencilComp] Pass replace }
|
然后面部材质球面板中如此设置,意为面部必然通过模板测试,且会将模板值改为128
128这数字是笔者随便挑的,没什么特殊含义。
之后便添加一个RenderFeature,专门用于绘制头发
关于一些细节的基础内容笔者已在上一篇文章中阐述过,这次就直接贴代码,不过多解释了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.Universal;
public class CelHairShadow_Stencil : ScriptableRendererFeature { [System.Serializable] public class Setting { public Color hairShadowColor; [Range(0, 0.1f)] public float offset = 0.02f; [Range(0, 255)] public int stencilReference = 1; public CompareFunction stencilComparison;
public RenderPassEvent passEvent = RenderPassEvent.BeforeRenderingTransparents; public LayerMask hairLayer; [Range(1000, 5000)] public int queueMin = 2000;
[Range(1000, 5000)] public int queueMax = 3000; public Material material;
} public Setting setting = new Setting(); class CustomRenderPass : ScriptableRenderPass { public ShaderTagId shaderTag = new ShaderTagId("UniversalForward"); public Setting setting;
FilteringSettings filtering; public CustomRenderPass(Setting setting) { this.setting = setting;
RenderQueueRange queue = new RenderQueueRange(); queue.lowerBound = Mathf.Min(setting.queueMax, setting.queueMin); queue.upperBound = Mathf.Max(setting.queueMax, setting.queueMin); filtering = new FilteringSettings(queue, setting.hairLayer); } public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor) { setting.material.SetColor("_Color", setting.hairShadowColor); setting.material.SetInt("_StencilRef", setting.stencilReference); setting.material.SetInt("_StencilComp", (int)setting.stencilComparison); setting.material.SetFloat("_Offset", setting.offset); }
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { var draw = CreateDrawingSettings(shaderTag, ref renderingData, renderingData.cameraData.defaultOpaqueSortFlags); draw.overrideMaterial = setting.material; draw.overrideMaterialPassIndex = 0;
var visibleLight = renderingData.cullResults.visibleLights[0]; Matrix4x4 worldToScreen = renderingData.cameraData.camera.worldToCameraMatrix; Vector2 lightDirSS = renderingData.cameraData.camera.worldToCameraMatrix * (visibleLight.localToWorldMatrix.GetColumn(2)); setting.material.SetVector("_LightDirSS", lightDirSS);
CommandBuffer cmd = CommandBufferPool.Get("DrawHairShadow"); context.ExecuteCommandBuffer(cmd); context.DrawRenderers(renderingData.cullResults, ref draw, ref filtering); }
public override void FrameCleanup(CommandBuffer cmd) {
} }
CustomRenderPass m_ScriptablePass;
public override void Create() { m_ScriptablePass = new CustomRenderPass(setting);
m_ScriptablePass.renderPassEvent = setting.passEvent; } public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) { if (setting.material != null) renderer.EnqueuePass(m_ScriptablePass); } }
|
在外面的面板中如此设置即可,当然头发还是得设置一下Layer为Hair
之后便是指定Material,使用这个Shader即可,可见内容相当简单
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
| Shader "Custom/HairShadow" { Properties { _Color ("Color", Color) = (1, 1, 1, 1) _Offset ("Offset", float) = 0.02 [Header(Stencil)] _StencilRef ("_StencilRef", Range(0, 255)) = 0 [Enum(UnityEngine.Rendering.CompareFunction)]_StencilComp ("_StencilComp", float) = 0 } SubShader { Tags { "RenderType" = "Opaque" "RenderPipeline" = "UniversalRenderPipeline" } HLSLINCLUDE #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" CBUFFER_START(UnityPerMaterial) float4 _Color; float _Offset; float4 _LightDirSS; CBUFFER_END ENDHLSL
Pass { Name "HairShadow" Tags { "LightMode" = "UniversalForward" } Stencil { Ref [_StencilRef] Comp [_StencilComp] Pass keep }
ZTest LEqual ZWrite Off HLSLPROGRAM
#pragma vertex vert #pragma fragment frag struct a2v { float4 positionOS: POSITION; float4 color: COLOR; }; struct v2f { float4 positionCS: SV_POSITION; float4 color: COLOR; }; v2f vert(a2v v) { v2f o; VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz); o.positionCS = positionInputs.positionCS;
float2 lightOffset = normalize(_LightDirSS.xy); lightOffset.y = lightOffset.y * _ProjectionParams.x; o.positionCS.xy += lightOffset * _Offset;
o.color = v.color; return o; } half4 frag(v2f i): SV_Target { return _Color; } ENDHLSL
} } }
|
于是我们就可以获得一个面部有刘海投影的效果了
由于自带的深度测试,直接避免了上一篇文章中的许多问题。
这样做的优缺点也比较显然:
优点
- 规避了额外绘制Buffer,无需切换RT
- 精度与使用RT写入深度进行深度判断相比更加高
缺点
- “阴影”的绘制与光照着色及人物贴图完全无关,导致在许多情况下会显得突兀
那么笔者也截几个图给大家看看这个缺点比较明显的时候
说白了就是因为刘海投影只使用了一个暗色,而面部是用面部贴图乘以暗部颜色的,只要面部贴图不是赛璐璐风格的纯色,便无法避免两者在结果上的差异。
改良-以性能换效果
问题不就是出在咱们没有本来的贴图颜色吗,那大不了再画一次脸,这次直接就是贴图色乘以暗色,总没问题了吧。
那么我们将头发的Pass修改一下,使用ColorMask 0
让它不再画入颜色,且将模板值重置为0
1 2 3 4 5 6 7 8 9 10
| Stencil { Ref [_StencilRef] Comp [_StencilComp] Pass Zero }
ZTest LEqual ZWrite Off ColorMask 0
|
于是我们给这个Shader再添加一个Pass,用于重新渲染面部:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| Pass { Name "HairShadow_Face" Tags { "LightMode" = "UniversalForward" } Stencil { Ref 0 Comp [_StencilComp] Pass keep }
ZTest LEqual ZWrite Off HLSLPROGRAM
#pragma vertex vert #pragma fragment frag struct a2v { float4 positionOS: POSITION; float4 color: COLOR; float2 uv: TEXCOORD; }; struct v2f { float4 positionCS: SV_POSITION; float4 color: COLOR; float2 uv: TEXCOORD0; }; v2f vert(a2v v) { v2f o; VertexPositionInputs positionInputs = GetVertexPositionInputs(v.positionOS.xyz); o.positionCS = positionInputs.positionCS;
o.color = v.color; o.uv = v.uv; return o; } TEXTURE2D(_FaceTex); SAMPLER(sampler_FaceTex);
half4 frag(v2f i): SV_Target { return SAMPLE_TEXTURE2D(_FaceTex, sampler_FaceTex, i.uv) * _Color; } ENDHLSL
}
|
然后在RenderFeature中增加面部贴图的指定,以及面部的再绘制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class Setting { public Texture faceTex; public LayerMask faceLayer;
}
class CustomRenderPass : ScriptableRenderPass { FilteringSettings filtering2; public CustomRenderPass(Setting setting) { filtering2 = new FilteringSettings(queue, setting.faceLayer); }
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData) { setting.material.SetTexture("_FaceTex", setting.faceTex); draw.overrideMaterialPassIndex = 1; context.DrawRenderers(renderingData.cullResults, ref draw, ref filtering2); } }
|
于是现在,我们终于能获得一个相对理想的结果了
当然,这样结果比较理想也只是因为面部的渲染算法比较简单,如果还有边缘光或者其他什么操作,大概就得真的重新用角色材质重新画一次了。
那么最后放个效果视频
结语
模板测试的存在感比较低,大家似乎都不易想到用它来实现这个Trick,而之前需要额外绘制RT的方法如今优化后应当也相对容易落地了些。
希望本文能够给在卡通渲染领域耕耘的人们带来些许启发。
参考资料
俊虎:Unity ShaderLab 模板缓存(Stencil Buffer) 基本概念
Writing shaders for different graphics APIs
感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。